/*
 * datatandroid - Persistence framework for Android platform
 * 
 * Copyright (C) 2010 ProjectsException
 * <http://code.google.com/p/datandroid>
 * 
 * This file is part of datatandroid.
 * 
 * datatandroid is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or 
 * (at your option) any later version.
 * 
 * datatandroid is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with datatandroid.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.projectsexception.datandroid.util;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import android.database.Cursor;

import com.projectsexception.datandroid.DatandroidException;
import com.projectsexception.datandroid.Field;
import com.projectsexception.datandroid.Table;
import com.projectsexception.datandroid.database.CollectionBasicProperty;
import com.projectsexception.datandroid.database.CollectionEntityProperty;
import com.projectsexception.datandroid.database.Entity;
import com.projectsexception.datandroid.database.EntityProperty;
import com.projectsexception.datandroid.database.IBasicProperty;
import com.projectsexception.datandroid.database.ICollectionProperty;
import com.projectsexception.datandroid.database.IProperty;

public class DatabaseUtil {

    private static String dateFormat = "yyyy-MM-dd HH:mm";
    private static final Map<String, Set<String>> types;
    private static Set<String> supportedIds;
    
    static {
        supportedIds = new HashSet<String>();
        supportedIds.add("java.lang.Integer");
        supportedIds.add("java.lang.Long");
        supportedIds.add("int");
        supportedIds.add("long");
        
        types = new HashMap<String, Set<String>>();        
        types.put("TEXT", new HashSet<String>());
        types.get("TEXT").add("java.lang.String");
        types.get("TEXT").add("java.util.Date");
        types.get("TEXT").add("java.lang.Enum");
        
        types.put("VARCHAR(1)", new HashSet<String>());
        types.get("VARCHAR(1)").add("java.lang.Character");
        types.get("VARCHAR(1)").add("char");
        
        types.put("INTEGER", new HashSet<String>());
        types.get("INTEGER").add("java.lang.Long");
        types.get("INTEGER").add("java.lang.Integer");
        types.get("INTEGER").add("java.lang.Short");
        types.get("INTEGER").add("long");
        types.get("INTEGER").add("int");
        types.get("INTEGER").add("short");
        types.get("INTEGER").add("java.lang.Boolean");
        types.get("INTEGER").add("boolean");
        
        types.put("DOUBLE", new HashSet<String>());
        types.get("DOUBLE").add("java.lang.Double");
        types.get("DOUBLE").add("double");
        
        types.put("FLOAT", new HashSet<String>());
        types.get("FLOAT").add("java.lang.Float");
        types.get("FLOAT").add("float");
    }
    
    public static Collection<Table> generateTables(Collection<Entity> entities) throws DatandroidException {
        Map<String, Table> tables = new HashMap<String, Table>();
        // Mapa donde la clave es la clase de la entidad y los valores es una lista de claves externas
        Map<String, List<String>> clavesExternas = new HashMap<String, List<String>>();
        for (Entity entity : entities) {
            Table t = new Table();
            t.setName(entity.getTableName());
            List<Field> fields = new ArrayList<Field>();
            t.setFields(fields);
            // Clave primaria
            IBasicProperty basicProp = entity.getIdProperty();
            Field f = new Field();
            f.setName(basicProp.getColumnName());
            f.setType(searchType(basicProp.getClassName()));
            f.setPrimaryKey(true);
            f.setObligatory(true);
            t.setPrimaryKey(f);
            tables.put(entity.getClassName(), t);
            // Resto de propiedades
            if (entity.getProperties() != null) {
                for (IProperty prop : entity.getProperties()) {
                    if (prop instanceof ICollectionProperty) {
                        ICollectionProperty colProp = (ICollectionProperty) prop;
                        
                        if (prop instanceof CollectionBasicProperty) {
                            // Se trata de una colección de tipos básicos luego no precisa de columna
                            // sino de una tabla propia con los campos ID, id de entidad padre y valor
                            
                            Table subT = new Table();
                            subT.setName(((CollectionBasicProperty) colProp).getTableName());
                            Field subF = new Field();
                            subF.setName("ID");
                            subF.setType("INTEGER");
                            subF.setObligatory(true);
                            subF.setPrimaryKey(true);
                            
                            subT.setPrimaryKey(subF);
                            subT.setFields(new ArrayList<Field>());
                            
                            subF = new Field();
                            subF.setName(entity.getTableName() + "_ID");
                            subF.setObligatory(true);
                            subF.setPrimaryKey(false);
                            subF.setType(searchType(entity.getIdProperty().getClassName()));
                            subT.getFields().add(subF);
                            
                            subF = new Field();
                            subF.setName("VALUE");
                            subF.setObligatory(true);
                            subF.setPrimaryKey(false);
                            subF.setType(searchType(colProp.getClassItem()));
                            subT.getFields().add(subF);
                           
                            tables.put(entity.getClassName() + "_" + colProp.getClassItem(), subT);                            
                        } else {
                            // Se trata de una coleccion de entidades, luego lo que tenemos que hacer
                            // es añadir una columna a la entidad en concreto
                            if (!clavesExternas.containsKey(colProp.getClassItem())) {
                                clavesExternas.put(colProp.getClassItem(), new ArrayList<String>());
                            }
                            clavesExternas.get(colProp.getClassItem()).add(((CollectionEntityProperty) colProp).getColumnName());
                        }
                    } else if (prop instanceof IBasicProperty) {
                        // Aquí entran tanto las propiedades básicas como las propiedades entidad
                        // ambas precisan de una columna en la tabla padre
                        basicProp = (IBasicProperty) prop;
                        f = new Field();
                        f.setName(basicProp.getColumnName());
                        f.setObligatory(!basicProp.getAllowNull());
                        f.setPrimaryKey(false);                        
                        if (prop instanceof EntityProperty) {
                            // Aquí entran las propiedades entidad luego este campo es la clave externa
                            f.setType("INTEGER");
                        } else {
                            // Sólo las propiedades básicas
                            f.setType(searchType(basicProp.getClassName()));
                        }
                        fields.add(f);
                    }
                }
            }
        }
        // Ahora añadimos los campos necesarios de claves externas
        List<String> claves;
        Field f;
        for (String className : clavesExternas.keySet()) {
            claves = clavesExternas.get(className);
            for (String clave : claves) {
                f = new Field();
                f.setName(clave);
                f.setObligatory(false);
                f.setPrimaryKey(false);
                f.setType("INTEGER");
                tables.get(className).getFields().add(f);
            }
        }
        return tables.values();
    }

    public static String createSQLTable(Table table) {
        StringBuilder sb = new StringBuilder("CREATE TABLE IF NOT EXISTS ");
        sb.append(table.getName());
        sb.append(" (");
        sb.append(table.getPrimaryKey().getName());
        sb.append(" ");
        sb.append(table.getPrimaryKey().getType());
        sb.append(" PRIMARY KEY AUTOINCREMENT");
        
        for (Field field : table.getFields()) {
            sb.append(", ");
            sb.append(createSQLType(field));
        }
        
        sb.append(");");
        return sb.toString();
    }
    
    public static String formatDate(Date date) {
        SimpleDateFormat format = new SimpleDateFormat(dateFormat);
        return format.format(date);
    }
    
    public static Date parseDate(String date) {
        SimpleDateFormat format = new SimpleDateFormat(dateFormat);
        try {
            return format.parse(date);
        } catch (ParseException e) {
            return null;
        }
    }
    
    public static boolean isDateType(String className) {
        return "java.util.Date".equals(className);
    }
    
    public static boolean isBasicType(String className) throws DatandroidException {
        return searchType(className) != null;
    }
    
    public static boolean isBasicType(Class<?> clazz) throws DatandroidException {
        return searchType(clazz) != null;
    }
    
    @SuppressWarnings("unchecked")
    public static String getDatabaseValue(Object obj) {
        if (obj instanceof Date) {
            return DatabaseUtil.formatDate((Date) obj);
        } else if (obj instanceof Boolean) {
            return ((Boolean) obj) ? "1" : "0";
        } else if (obj instanceof Enum) {
            Enum objEnum = (Enum) obj;
            return objEnum.name();
        } else {
            return obj.toString();
        }
    }
    
    public static Object getColumnValue(Cursor c, int pos, String className) throws DatandroidException {
        String type = DatabaseUtil.searchType(className);
        if (type.equals("INTEGER")) {
            if (className.equals("boolean") || className.equals("java.lang.Boolean")) {
                return c.getInt(pos) == 1;
            } else {
                return c.getInt(pos);
            }
        } else if (type.equals("DOUBLE")) {
            return c.getDouble(pos);
        } else if (type.equals("FLOAT")) {
            return c.getFloat(pos);
        } else if (type.equals("TEXT")) {
            String result = c.getString(pos);
            return getColumnTextValue(className, result);
        }
        return null;
    }
    
    public static String searchType(String className) throws DatandroidException {
        try {
            String wrapper = ReflectionUtil.getWrapperPrimitive(className);
            Class<?> classObj;
            if (wrapper != null) {
                classObj = Class.forName(wrapper);
            } else {
                classObj = Class.forName(className);
            }
            return searchType(classObj);
        } catch(ClassNotFoundException e) {
            throw new DatandroidException(e);
        }
    }
    
    public static String searchType(Class<?> clazz) throws DatandroidException {
        for (String type : types.keySet()) {
            if (types.get(type).contains(clazz.getName())) {
                return type;
            }
        }
        // No hemos encontrado la clase exacta, pero puede ser una subclase
        Class<?> classTypeObj;
        try {
            for (String type : types.keySet()) {
                for (String classType : types.get(type)) {
                    if (classType.indexOf('.') > 0) {
                        classTypeObj = Class.forName(classType);
                        if (classTypeObj.isAssignableFrom(clazz)) {
                            return type;
                        }
                    }
                }
            }
        } catch(ClassNotFoundException e) {
            throw new DatandroidException(e);
        }
        return null;
    }
    
    public static boolean supportedId(Class<?> clazz) {
        return supportedIds.contains(clazz.getName());
    }
    
    @SuppressWarnings("unchecked")
    private static Object getColumnTextValue(String className, String value) throws DatandroidException {
        Class<?> clazz;
        try {
            clazz = Class.forName(className);
        } catch (ClassNotFoundException e) {
            throw new DatandroidException(e);
        }
        if (Date.class.isAssignableFrom(clazz)) {
            return DatabaseUtil.parseDate(value);
        } else if(Enum.class.isAssignableFrom(clazz)) {
            Class<? extends Enum> c = (Class<? extends Enum>) clazz;
            try {
                return Enum.valueOf(c.getEnumConstants()[0].getClass(), value);
            } catch(IllegalArgumentException e) {
                throw new DatandroidException(e);
            }
        } else {
            return value;
        }
    }
    
    private static String createSQLType(Field field) {
        StringBuilder sb = new StringBuilder();
        sb.append(field.getName());
        sb.append(" ");
        sb.append(field.getType());
        if (field.isObligatory()) {
            sb.append(" NOT NULL");
        }
        return sb.toString();        
    }

}
